EPub格式电子书的制作

最近在写的一个项目涉及到epub格式电子书的制作,借这个机会总结一下epub这个电子图书标准,并利用Python语言生成一本简单的epub格式电子书。

EPub的前世今生

为什么会有EPub

我曾说过,电子书的阅读越来越流行是未来阅读发展的不可避免的趋势。纸质书籍是否会在历史长河中消失我们无从知晓,但可以确定的是,数字化阅读在未来至少十年中,会润物细无声般成为更多人的一种生活方式。

或许很多人都没有察觉到,我们这一代经历的正是一场关于人类获取信息,生产内容方式的巨变。从因特网诞生,电子邮件、超链接、富文本的广泛使用,再到现在所谓的”互联网2.0”,人们从“下载者”转变为“上传者”,这场转变的发展也不过是数十年而已,说到这,想起一张著名的图片:
比尔盖茨拿着光盘

这张光盘能装下的信息比下面所有纸能记录下的都多

–比尔盖茨,1994

随着技术的发展,人们开始不满足于简单的文本书籍,于是富文本格式开始出现(就网页浏览来说,可以理解为HTML是骨架,CSS是皮肤,JavaScript是动作,Wold当然也算,但不够开放通用),这不仅仅是表现形式的变化,交互性也开始展现了。在这个过程中,EPub作为一种自由的电子书开放标准,自然而然地孕育而生,也自然而然地进化着。
我们为什么需要EPub?我想一篇文章中的一段比我说的更好:

EPUB enables content to be created by an author or publisher once, via different tools and services, distributed through many channels, and viewed, online or offline, using many different devices and applications. The EPUB specifications form a kind of “contract” between content creators and reading systems to enable this interoperability.

来自epubzone上的一篇文章

所以,EPub是什么

简单来说, EPub格式是一种电子书的标准,事实上几乎成为了行业标准,注意观察的话,几乎所有的电子书阅读器,从硬件到软件,都支持EPub格式的电子书(Kindle是一朵奇葩,原生系统不支持EPub,因为它要推自己的Mobi格式)。更具体的内容可以查wikipedia-EPUB, 这里说个好玩的吧,EPub格式电子书采用zip压缩格式来包裹书籍内容以及格式控制的文件(因为遵循IDPF推出的OCF规范,而OCF规范遵循ZIP压缩技术),所以我们可以把.epub改成.zip,然后解压缩,直接阅读书籍的内容,这样一来,在PC | Mac上,没有EPub阅读器,照样可以打开EPub阅读。

怎么构建一本EPub格式的电子书

上面提到,EPub格式的电子书其实是一个压缩包文件,里面有几个按照规范定义的文件,所谓标准,就是规范EPub文件中某些文件的格式、内容和位置等等。因此,如果我们想要自己制作一本EPub格式的电子书,首先要了解要制作的内容压缩为zip文件前的文件结构是什么,一个典型的EPub的文件结构是这样的:

IBM epub

其实结构可以更简单,下面给出我用Python语言构建的EPub文件的文件结构:
my epub

对比一下可以看出来有些文件并不是必须的,下面简单介绍一下EPub文件的目录结构:

mimetype文件

这个内容是固定的,就一行
application/epub+zip

表明可以被EPub工具打开或zip工具打开

META-INF文件夹

根据OCF(Open Container Format)标准,该文件夹包含一个文件container.xml,内容如下:

<?xml version="1.0" encoding="UTF-8" ?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
   <rootfiles> 
       <rootfile full-path="OPS/content.opf" media-type="application/oebps-package+xml"/> </rootfiles>
</container>

它的功能是告诉阅读器电子书根文件路径以及打开方式,如果你修改了content.opf的名字或者把它放在其他位置,应该写明完整的路径。

OEBPS文件夹

OEBPS目录用于存放OPS文档、OPF文档、CSS文档、NCX文档, OEBPS这个名字是可变的,可以根据containter.xml进行配置。这里是OPS文件夹。

opf文件:

content.opf文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8" ?>
<package version="2.0" unique-identifier="PrimaryID" xmlns="http://www.idpf.org/2007/opf">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
<dc:title>thisisbooktitle</dc:title>
<dc:creator>frank</dc:creator>
<dc:description>this is description</dc:description>
<meta name="cover" content="cover"/>
</metadata>
<manifest>
<item id='chapter1.html' href='chapter1.html' media-type='application/xhtml+xml'/>
<item id='chapter2.html' href='chapter2.html' media-type='application/xhtml+xml'/>
<item id="ncx" href="content.ncx" media-type="application/x-dtbncx+xml"/>
<item id="cover" href="cover.jpg" media-type="image/jpeg"/>
</manifest>
<spine toc="ncx">
<itemref idref='chapter1.html'/>
<itemref idref='chapter2.html'/>
</spine>
</package>

这是一个标准的XML文件,遵循OPF规范,主要属性有:

  • metadata
    包括dc-metadata和x-metadata,dc-metadata有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <title>:题名
    <creator>:责任者
    <subject>:主题词或关键词
    <description>:内容描述
    <contributor>:贡献者或其它次要责任者
    <date>:日期
    <type>:类型
    <format>:格式
    <identifier>:标识符
    <source>:来源
    <language>:语种
    <relation>:相关信息
    <coverage>:履盖范围
    <rights>:权限描述

    如果是未知属性可以用x-metadata描述

  • menifest
    文件列表, 列出OEBPS文档及相关的文档,由一个子元素构成,,该元素由三个属性构成:

1
2
3
id:表示文件的ID号
href:文件的相对路径
media-type:文件的媒体类型
  • spine toc=”ncx”
    表明书籍的阅读次序,其中有一个元素itemref idref=””,idref是menifest中的id
  • opf还有很多其他属性,实际中用的并不多,即使用到也是一目了然的,如有需要可以连猜带蒙+搜索引擎。
ncx文件

content.ncx文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<head>
<meta name="dtb:uid" content=" "/>
<meta name="dtb:depth" content="-1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle><text>thisisbooktitle</text></docTitle>
<docAuthor><text>frank</text></docAuthor>
<navMap>
<navPoint id='chapter1.html' class='level1' playOrder='1'>
<navLabel> <text>chapter1.html</text> </navLabel>
<content src='chapter1.html'/></navPoint>
<navPoint id='chapter2.html' class='level1' playOrder='2'>
<navLabel> <text>chapter2.html</text> </navLabel>
<content src='chapter2.html'/></navPoint>
</navMap>
</ncx>

该文件的作用是描述电子书的目录结构,这里的content.ncx文件并没有很明显的体现。有兴趣的话可以解压一本EPub格式的电子书看一看。

最后,给出利用Python制作简单EPub文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import shutil

title = 'thisisbooktitle'
creator = 'frank'
description = 'this is description'

htmllist = ['chapter1.html', 'chapter2.html'] # 来自《亲爱的安德烈》中的两章

os.mkdir('tmp')

tmpfile = file('tmp/mimetype', 'w')
tmpfile.write('application/epub+zip')
tmpfile.close()

os.mkdir('tmp/META-INF')

tmpfile = file('tmp/META-INF/container.xml', 'w')
tmpfile.write('''<?xml version="1.0" encoding="UTF-8" ?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles> <rootfile full-path="OPS/content.opf" media-type="application/oebps-package+xml"/> </rootfiles>
</container>
''')
tmpfile.close()

os.mkdir('tmp/OPS')

if os.path.isfile('cover.jpg'): # 如果有cover.jpg, 用来制作封面
shutil.copyfile('cover.jpg', 'tmp/OPS/cover.jpg')
print 'Cover.jpg found!'

opfcontent = '''<?xml version="1.0" encoding="UTF-8" ?>
<package version="2.0" unique-identifier="PrimaryID" xmlns="http://www.idpf.org/2007/opf">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
%(metadata)s
<meta name="cover" content="cover"/>
</metadata>
<manifest>
%(manifest)s
<item id="ncx" href="content.ncx" media-type="application/x-dtbncx+xml"/>
<item id="cover" href="cover.jpg" media-type="image/jpeg"/>
</manifest>
<spine toc="ncx">
%(ncx)s
</spine>
</package>
'''

dc = '<dc:%(name)s>%(value)s</dc:%(name)s>'
item = "<item id='%(id)s' href='%(url)s' media-type='application/xhtml+xml'/>"
itemref = "<itemref idref='%(id)s'/>"

metadata = '\n'.join([
dc % {'name': 'title', 'value': title},
dc % {'name': 'creator', 'value': creator},
dc % {'name': 'description', 'value': description},
])

manifest = []
ncx = []

for htmlitem in htmllist:
content = file(htmlitem, 'r').read()
tmpfile = file('tmp/OPS/%s' % htmlitem, 'w')
tmpfile.write(content)
tmpfile.close()
manifest.append(item % {'id': htmlitem, 'url': htmlitem})
ncx.append(itemref % {'id': htmlitem})

manifest='\n'.join(manifest)
ncx='\n'.join(ncx)

tmpfile = file('tmp/OPS/content.opf', 'w')
tmpfile.write(opfcontent %{'metadata': metadata, 'manifest': manifest, 'ncx': ncx,})
tmpfile.close()

ncx = '''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">
<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">
<head>
<meta name="dtb:uid" content=" "/>
<meta name="dtb:depth" content="-1"/>
<meta name="dtb:totalPageCount" content="0"/>
<meta name="dtb:maxPageNumber" content="0"/>
</head>
<docTitle><text>%(title)s</text></docTitle>
<docAuthor><text>%(creator)s</text></docAuthor>
<navMap>
%(navpoints)s
</navMap>
</ncx>
'''

navpoint = '''<navPoint id='%s' class='level1' playOrder='%d'>
<navLabel> <text>%s</text> </navLabel>
<content src='%s'/></navPoint>'''

navpoints = []
for i, htmlitem in enumerate(htmllist):
navpoints.append(navpoint % (htmlitem, i+1, htmlitem, htmlitem))

tmpfile = file('tmp/OPS/content.ncx', 'w')
tmpfile.write(ncx % {
'title': title,
'creator': creator,
'navpoints': '\n'.join(navpoints)})
tmpfile.close()

from zipfile import ZipFile
epubfile = ZipFile('book.epub', 'w')
os.chdir('tmp')
for d, ds, fs in os.walk('.'):
for f in fs:
epubfile.write(os.path.join(d, f))
epubfile.close()

shutil.rmtree("../tmp")

print ("Done")

说明:chapter1.html, chapter2.html 是我从“亲爱的安德烈.epub”中提取的两章,你也可以替换成其他的内容,若上述python代码存为simple_epub.py,将chapter1.htmlchapter2.html, simple_epub.py放在同一目录下, 通过python simple_epub.py 即可生成book.epub文件。

参考资料: